package top.cardone.security.shiro.web.filter.impl;

import com.google.common.collect.Lists;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.util.WebUtils;
import top.cardone.cache.Cache;
import top.cardone.context.ApplicationContextHolder;
import top.cardone.context.util.CodeExceptionUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.io.Writer;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Created by yht on 16-2-2.
 */
@Log4j2
public class KickoutSessionControlFilterImpl extends PathMatchingFilter {
	@Setter
	private String kickoutUrl = "/kickoutInfo.html"; //踢出后到的地址

	@Setter
	private String kickoutMessage = "您的帐号在另一处已登录!"; //踢出后到的地址

	@Setter
	private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户

	@Setter
	private int maxSession = 1; //同一个帐号最大会话数 默认1

	private String kickoutParam = "kickout";//踢出参数

	private String cacheParam = "shiro-kickout-session";//踢出缓存参数

	@Setter
	private List<String> usernameParams = Lists.newArrayList("userCode", "userId");

	@Override
	protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
		Subject subject = SecurityUtils.getSubject();

		if (!subject.isAuthenticated() && !subject.isRemembered()) {
			//如果没有登录，直接进行之后的流程
			return true;
		}

		String username = null;


		Object principal = subject.getPrincipal();

		if (principal instanceof Map) {
			Map<String, Object> principalMap = (Map<String, Object>) principal;

			for (String usernameParam : usernameParams) {
				username = MapUtils.getString(principalMap, usernameParam);

				if (!StringUtils.isBlank(username)) {
					break;
				}
			}

		} else {
			for (String usernameParam : usernameParams) {
				username = BeanUtils.getProperty(principal, usernameParam);

				if (!StringUtils.isBlank(username)) {
					break;
				}
			}
		}


		if (StringUtils.isBlank(username)) {
			return true;
		}

		Deque<Serializable> deque = ApplicationContextHolder.getBean(Cache.class).get(cacheParam, username, () -> new LinkedList<>());

		Session session = subject.getSession();

		Serializable sessionId = session.getId();

		//如果队列里没有此sessionId，且用户没有被踢出；放入队列
		if (!deque.contains(sessionId) && session.getAttribute(kickoutParam) == null) {
			deque.push(sessionId);

			ApplicationContextHolder.getBean(Cache.class).put(cacheParam, username, deque);
		}

		//如果队列里的sessionId数超出最大会话数，开始踢人
		while (deque.size() > maxSession) {
			Serializable kickoutSessionId;

			if (kickoutAfter) { //如果踢出后者
				kickoutSessionId = deque.removeFirst();
			} else { //否则踢出前者
				kickoutSessionId = deque.removeLast();
			}

			try {
				Session kickoutSession = SecurityUtils.getSecurityManager().getSession(new DefaultSessionKey(kickoutSessionId));

				if (kickoutSession != null) {
					//设置会话的kickout属性表示踢出了
					kickoutSession.setAttribute(kickoutParam, true);
				}
			} catch (Exception e) {//ignore exception
				log.error(e.getMessage(), e);
			}

			ApplicationContextHolder.getBean(Cache.class).put(cacheParam, username, deque);
		}

		if (session.getAttribute(kickoutParam) == null) {
			return true;
		}

		try {
			subject.logout(); //会话被踢出了
		} catch (Exception e) { //ignore
			log.error(e.getMessage(), e);
		}

		if (!org.apache.commons.lang3.StringUtils.startsWith(request.getContentType(), org.springframework.http.MediaType.APPLICATION_JSON_VALUE)) {
			WebUtils.saveRequest(request);

			WebUtils.issueRedirect(request, response, kickoutUrl);

			return false;
		}

		response.setCharacterEncoding(CharEncoding.UTF_8);
		response.setContentType(org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE);

		try (Writer out = response.getWriter()) {
			String requestURI = getPathWithinApplication(request);

			String json = CodeExceptionUtils.newString(requestURI, "000002", kickoutMessage);

			out.write(json);

			out.flush();
		} catch (java.io.IOException e) {
			log.error(e.getMessage(), e);
		}

		return false;
	}
}
